package com.xzr.demo.mqserver.core;

import com.xzr.demo.common.MqException;

/**
 * 实现 交换机的转发规则
 * 同时验证 bindingKey是否合法
 *
 * @Author：徐志荣
 * @Date：2023/8/14 上午 9:59
 */
public class Router {
    /**
     * @param bindingKey 构造规则：1. 数字、字母、下划线  2. 使用 . 分割成若干部分 3. 允许存在 *和# ，但是通配符只能作为独立的分段
     * @return
     */
    public boolean checkBindingKey(String bindingKey) {
        if (bindingKey.length() == 0) {
            //空字符串也是合法的，比如 direct/fanout 交换机的时候，bindingKey是用不上的
            return true;
        }
        for (int i = 0; i < bindingKey.length(); i++) {
            char ch = bindingKey.charAt(i);
            //判定字符是否是大写字母
            if (ch >= 'A' && ch <= 'Z') {
                continue;
            }
            //判定该字母是否是小写字母
            if (ch >= 'a' && ch <= 'z') {
                continue;
            }
            //判定该字母是阿拉伯数字
            if (ch >= '0' && ch <= '9') {
                continue;
            }
            //判定是否是 _ 或者 .
            if (ch == '_' || ch == '.' || ch == '*' || ch == '#') {
                continue;
            }
            //该字符，不是上述任何一种合法情况，就直接返回 false
            return false;
        }
        //检查 * # 是否是独立的部分
        String[] words = bindingKey.split("\\.");
        for (String word : words) {
            // 检查 word 长度 > 1 并且包含了 * 或者 # , 就是非法的格式了.
            if (word.length() > 1 && (word.contains("*") || word.contains("#"))) {
                return false;
            }
        }
        // 约定一下, 通配符之间的相邻关系(人为(俺)约定的).
        // 为啥这么约定? 因为前三种相邻的时候, 实现匹配的逻辑会非常繁琐, 同时功能性提升不大~~
        // 1. aaa.#.#.bbb    => 非法
        // 2. aaa.#.*.bbb    => 非法
        // 3. aaa.*.#.bbb    => 非法
        // 4. aaa.*.*.bbb    => 合法
        for (int i = 0; i < words.length - 1; i++) {
            // 连续两个 ##
            if (words[i].equals("#") && words[i + 1].equals("#")) {
                return false;
            }
            // # 连着 *
            if (words[i].equals("#") && words[i + 1].equals("*")) {
                return false;
            }
            // * 连着 #
            if (words[i].equals("*") && words[i + 1].equals("#")) {
                return false;
            }
        }

        return true;
    }

    /**
     * @param routingKey 构造规则：1. 数字、字母、下划线  2. 使用 . 分割成若干部分
     * @return
     */
    public boolean checkRoutingKey(String routingKey) {
        if (routingKey.length() == 0) {
            //空字符串 ， 合法的情况，比如使用 fanout 交换机的时候， routingKey用不上
            return true;
        }
        for (int i = 0; i < routingKey.length(); i++) {
            char ch = routingKey.charAt(i);
            //判定字符是否是大写字母
            if (ch >= 'A' && ch <= 'Z') {
                continue;
            }
            //判定该字母是否是小写字母
            if (ch >= 'a' && ch <= 'z') {
                continue;
            }
            //判定该字母是阿拉伯数字
            if (ch >= '0' && ch <= '9') {
                continue;
            }
            //判定是否是 _ 或者 .
            if (ch == '_' || ch == '.') {
                continue;
            }
            //该字符，不是上述任何一种合法情况，就直接返回 false
            return false;
        }
        //把每个字符都检查过了，也没有返回，说明是 合法的
        return true;
    }

    /**
     * 这个方法用来判定 该消息是否 可以转发给这个绑定对应的队列
     *
     * @param exchangeType
     * @param binding
     * @param message
     * @return
     */
    public boolean route(ExchangeType exchangeType, Binding binding, Message message) throws MqException {
        //根据不同的exchangeType 使用不同的判定转发规则
        if (exchangeType == ExchangeType.FANOUT) {
            //如果是 FANOUT ，则
            return true;
        } else if (exchangeType == ExchangeType.TOPTIC) {
            //如果是 主题交换机，规则就要更复杂一些
            return routeTopic(binding, message);
        } else {
            //其他情况是不应该存在的
            throw new MqException("[Router] 交换机类型非法，.exchangeType = " + exchangeType);
        }

    }

    /**
     * 转发规则实现
     *
     * @param binding
     * @param message
     * @return
     */
    private boolean routeTopic(Binding binding, Message message) {
        //先把这两个key进行切分
        String[] bindingTokens = binding.getBindingKey().split("\\.");
        String[] routingTokens = message.getRoutingKey().split("\\.");
        //引入两个下标，指向上述两个数组，初始情况下都为0
        int bindingIndex = 0;
        int routingIndex = 0;
        while (bindingIndex < bindingTokens.length && routingIndex < routingTokens.length) {
            if (bindingTokens[bindingIndex].equals("*")) {
                //【情况2】如果遇到了 * ， 直接进行下一轮， * 可以匹配任意一个部分
                bindingIndex++;
                routingIndex++;
                continue;
            } else if (bindingTokens[bindingIndex].equals("#")) {
                //【情况3】如果遇到 #， 需要先看看有没有下一个位置
                bindingIndex++;
                if (bindingIndex == bindingTokens.length) {
                    //该 # 后面没东西了，一定可以匹配成功
                    return true;
                }
                //【情况4】后面还有内容，去routingKey中往后找，找到对应位置
                routingIndex = findNextMatch(routingTokens, routingIndex, bindingTokens[bindingIndex]);
                if (routingIndex == -1) {
                    //没找到
                    return false;
                }
                //找到了匹配的情况，继续往后匹配
                bindingIndex++;
                routingIndex++;
            } else {
                // 【情况1】 如果遇到普通字符串， 要求两边内容是一样的
                if (!bindingTokens[bindingIndex].equals(routingTokens[routingIndex])) {
                    return false;
                }
                bindingIndex++;
                routingIndex++;
            }
        }
        //【情况5】判定是否是双方同时到达末尾
        // aaa.bbb.ccc    aaa.bbb
        if (bindingIndex == bindingTokens.length && routingIndex == routingTokens.length) {
            return true;
        }
        return false;
    }

    /**
     * 查找该部分，在 routingKey 的位置，返回该下标，没找到，就返回 -1
     *
     * @param routingTokens
     * @param routingIndex
     * @param bindingToken
     * @return
     */
    private int findNextMatch(String[] routingTokens, int routingIndex, String bindingToken) {
        for (int i = routingIndex; i < routingTokens.length; i++) {
            if (routingTokens[i].equals(bindingToken)) {
                return i;
            }
        }
        return -1;
    }


}
